/*
 * Copyright (c) 2009, Hamish Morgan. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the University of Sussex nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
package locusts.client.diamondtouch;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.HierarchyBoundsListener;
import java.awt.event.HierarchyEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;

/**
 * Class that holds a mapping from table-space to screen space. Used to
 * convert points in touch events to and from table space.
 *
 * Some thoughts....
 * 
 * Could use and AffineTransform but would require solving a rather nasty
 * equasion. The table-space can be defined by translation, rotation,
 * scale and skew so and AffineTransform can be used. The user will
 * callibrate the table by clicking on a number of points on the screen.
 * Each point p will be rendered at a given x,y, and will correspond to
 * a touch point p' at (x',y'). The transform T can be user for moving
 * between these two spaces.
 *
 * P' = pT and p = pT' (where T' represents the inverse of T)
 *
 * So...
 *
 *  [ x']   [  m00  m01  m02  ] [ x ]   [ m00x + m01y + m02 ]
 *  [ y'] = [  m10  m11  m12  ] [ y ] = [ m10x + m11y + m12 ]
 *  [ 1 ]   [   0    0    1   ] [ 1 ]   [         1         ]
 *
 * We need to resolve the values of m in T. I think this should work as a
 * simultanious equasion (though I could be wrong.)
 *
 * hmm... need internet
 *
 * =====================================
 *
 * Actually it's not an affine transform, because paralel lines may not
 * remain paralel. This is because the project can (and generally will)
 * not be directly above the table.
 *
 *
 * ========================================
 *
 * Am now thinking that there are several levels of transform that form a
 * pipeline. There a number of different vector spaces to be considered:
 *  World space - The positions of the entities in the world model
 *  Screen space - The position of things on the screen
 *  Touch space - The position of touchs on the table
 *  Component space - The position of things relative to the rendering area.
 *  Projection space - Where things are actually projected on the table.
 *
 * 
 *
 * @author Hamish Morgan
 */
public class DTTransform {

    private final AffineTransform at;
    private Dimension table;
    private Dimension screen;
    private Rectangle canvas;
    private boolean clean;

    public DTTransform() {
        at = new AffineTransform();
        table = new Dimension();
        screen = new Dimension();
        canvas = new Rectangle();
        clean = true;
    }

    private void regenerateTransform() {
        at.setToIdentity();
        if (!canvas.isEmpty())
            at.translate(-canvas.x, -canvas.y);
        if (screen.width != 0 && screen.height != 0 && table.height != 0 &&
                table.width != 0) {
            at.scale((double) screen.width / (double) table.width,
                    (double) screen.height / (double) table.height);
        }
        clean = true;
    }

    public void transform(Point2D p) {
        if (!clean) regenerateTransform();
        at.transform(p, p);
    }

    public int transformX(int x) {
        // Assumes y = 0
        final Point2D p = new Point2D.Double(x, 0);
        transform(p);
        return (int) p.getX();
    }

    public int transformY(int y) {
        // Assumes y = 0
        final Point2D p = new Point2D.Double(0, y);
        transform(p);
        return (int) p.getY();
    }

    /**
     * The effective verticle resolution of the DiamonTouch table. This does
     * not correspont to the sensor desity, but rather fidelity with which
     * touch events can be distinquished,
     *
     * @return the verticle resolution
     */
    public int getTableHeight() {
        return table.height;
    }

    /**
     * The effective horizontal resolution of the DiamonTouch table. This does
     * not correspont to the sensor desity, but rather fidelity with which
     * touch events can be distinquished,
     *
     * @return the horizontal resolution
     */
    public int getTableWidth() {
        return table.width;
    }

    public int getScreenHeight() {
        return screen.height;
    }

    public int getScreenWidth() {
        return screen.width;
    }

    public int getCanvasX() {
        return canvas.x;
    }

    public int getCanvasY() {
        return canvas.y;
    }

    public int getCanvasWidth() {
        return canvas.width;
    }

    public int getCanvasHeight() {
        return canvas.height;
    }

    public void setScreenHeight(int height) throws IllegalArgumentException {
        if (height <= 0) throw new IllegalArgumentException(
                    "Height argument should be a possitive integer, found: " +
                    height);
        if (screen.height != height) {
            screen.height = height;
            clean = false;
        }
    }

    public void setScreenWidth(int width) throws IllegalArgumentException {
        if (width <= 0) throw new IllegalArgumentException(
                    "Width argument should be a possitive integer, found: " +
                    width);
        if (screen.width != width) {
            screen.width = width;
            clean = false;
        }
    }

    public void setScreenSize(int width, int height) throws
            IllegalArgumentException {
        setScreenWidth(width);
        setScreenHeight(height);
    }

    public void setScreenSize(Dimension dim) throws IllegalArgumentException,
            NullPointerException {
        setScreenSize(dim.width, dim.height);
    }

    public void setTableHeight(int height) throws IllegalArgumentException {
        if (height <= 0) {
            throw new IllegalArgumentException(
                    "Height should be a possitive integer, found: " +
                    height);
        }
        if (table.height != height) {
            table.height = height;
            clean = false;
        }
    }

    public void setTableWidth(int width) throws IllegalArgumentException {
        if (width <= 0)
            throw new IllegalArgumentException(
                    "Width should be a possitive integer, found: " +
                    width);
        if (table.width != width) {
            table.width = width;
            clean = false;
        }
    }

    public void setTableSize(int width, int height) throws
            IllegalArgumentException {
        setTableWidth(width);
        setTableHeight(height);
    }

    public void setTableSize(Dimension dim) throws IllegalArgumentException,
            NullPointerException {
        setTableSize(dim.width, dim.height);
    }

    public void setCanvasX(int x) {
        if (canvas.x != x) {
            canvas.x = x;
            clean = false;
        }
    }

    public void setCanvasY(int y) {
        if (canvas.y != y) {
            canvas.y = y;
            clean = false;
        }
    }

    public void setCanvasWidth(int width) {
        if (canvas.width != width) {
            canvas.width = width;
            clean = false;
        }
    }

    public void setCanvasHeight(int height) {
        if (canvas.height != height) {
            canvas.height = height;
            clean = false;
        }
    }

    public void setCanvasPosition(int x, int y) {
        setCanvasX(x);
        setCanvasY(y);
    }

    public void setCanvasPosition(Point p) {
        setCanvasPosition(p.x, p.y);
    }

    public void setCanvasSize(int width, int height) {
        setCanvasWidth(width);
        setCanvasHeight(height);
    }

    public void setCanvasSize(Dimension dim) {
        setCanvasSize(dim.width, dim.height);
    }

    public void setCanvasBounds(int x, int y, int w, int h) {
        setCanvasPosition(x, y);
        setCanvasSize(w, h);
    }

    public void setCanvasBounds(Rectangle bounds) {
        setCanvasBounds(bounds.x, bounds.y, bounds.width, bounds.height);
    }

    private void updateBounds(Component c) {
//        System.out.println("===================================================");
//        System.out.println("screensize: " + c.getToolkit().getScreenSize());
//        System.out.println("visible: " + c.isVisible());
//        System.out.println("showing: " + c.isShowing());
//        if (c.isVisible() && c.isShowing())
//            System.out.println("location on screen: " + c.getLocationOnScreen());
//        System.out.println("");
        setScreenSize(c.getToolkit().getScreenSize());
        if (c.isVisible() && c.isShowing()) {
            setCanvasPosition(c.getLocationOnScreen());
            setCanvasSize(c.getSize());
        }
    }

    public void watchComponent(Component c) {
        c.addHierarchyBoundsListener(new BoundsListener());
        updateBounds(c);
    }

    private class BoundsListener implements HierarchyBoundsListener {

        public void ancestorMoved(HierarchyEvent e) {
            updateBounds(e.getComponent());
        }

        public void ancestorResized(HierarchyEvent e) {
            updateBounds(e.getComponent());
        }
    }
}
